/**
 * Copyright 2008 Adam Ruggles.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package kiff.util.filter;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Map;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import kiff.exception.FilterException;

import org.apache.xerces.parsers.DOMParser;
import org.apache.xerces.xni.parser.XMLDocumentFilter;
import org.cyberneko.html.HTMLConfiguration;
import org.cyberneko.html.filters.ElementRemover;
import org.cyberneko.html.filters.Purifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

/**
 * Neko Filter, uses CyberNeko HTML Parser to filter out or clean raw HTML text. 
 * @author Adam
 * @version $Id: NekoFilter.java 38 2008-09-21 07:40:51Z a.ruggles $
 */
public class NekoFilter implements TextFilter {
	/**
     * The <code>Logger</code> is used by the application to generate a log messages.
     */
    private final Logger logger = LoggerFactory.getLogger(NekoFilter.class);

    /**
     * The NekoHTML DOM Parser.
     */
	private final DOMParser parser = new DOMParser(new HTMLConfiguration());

	/**
	 * Constructs a Neko Filter with the default filters and settings. 
	 */
	public NekoFilter() {
		Purifier purifier = new Purifier();
		ElementRemover remover = new ElementRemover();
		remover.acceptElement("a", new String[] { "href" });
		remover.acceptElement("html", null);
		remover.acceptElement("br", null);
		remover.removeElement("script");
		XMLDocumentFilter[] filters = { purifier, remover };
		try {
			parser.setProperty("http://cyberneko.org/html/properties/filters", filters);
			initDefaultSettings();
		} catch (SAXNotRecognizedException e) {
			logger.error("SAXNotRecognizedException", e);
		} catch (SAXNotSupportedException e) {
			logger.error("SAXNotSupportedException", e);
		}
	}

	/**
	 * Constructs a NekoFilter with or without the purifier filter.
	 * @param enablePurifier If set to true it will enable the NekoHTML purifier filter.
	 */
	public NekoFilter(final boolean enablePurifier) {
		if (enablePurifier) {
			XMLDocumentFilter[] filters = { new Purifier() };
			try {
				parser.setProperty("http://cyberneko.org/html/properties/filters", filters);
			} catch (SAXNotRecognizedException e) {
				logger.error("SAXNotRecognizedException", e);
			} catch (SAXNotSupportedException e) {
				logger.error("SAXNotSupportedException", e);
			}
		}
		initDefaultSettings();
	}

	/**
	 * Constructs a NekoHTML Filter with the options to configure the ElementRemover filter and the Purifier filter.
	 * @param enablePurifier If set to true it will enable the NekoHTML purifier filter.
	 * @param acceptElements A Map of elements and attributes that will be allowed past the ElementRemover filter.
	 * @param removeElements A list of elements that should have anything wrapped by that element removed as well.
	 */
	public NekoFilter(final boolean enablePurifier, final Map<String, String> acceptElements,
			final String ... removeElements) {
		ElementRemover elementRemover = new ElementRemover();
		for (Map.Entry<String, String> entry : acceptElements.entrySet()) {
			if (entry.getValue() == null) {
				elementRemover.acceptElement(entry.getKey(), null);
			} else {
				elementRemover.acceptElement(entry.getKey(), entry.getValue().split("\\s*,\\s*"));
			}
		}
		for (String removeElement : removeElements) {
			elementRemover.removeElement(removeElement);
		}
		XMLDocumentFilter[] filters = null;
		if (enablePurifier) {
			filters = new XMLDocumentFilter[2];
			filters[0] = elementRemover;
			filters[1] = new Purifier();
		} else {
			filters = new XMLDocumentFilter[1];
			filters[0] = elementRemover;
		}
		try {
			parser.setProperty("http://cyberneko.org/html/properties/filters", filters);
		} catch (SAXNotRecognizedException e) {
			logger.error("SAXNotRecognizedException", e);
		} catch (SAXNotSupportedException e) {
			logger.error("SAXNotSupportedException", e);
		}
		initDefaultSettings();
	}

	/**
	 * @see kiff.util.filter.TextFilter#filter(java.lang.String)
	 */
	public String filter(final String text) throws FilterException {
		Writer writer = new StringWriter();
		Reader reader = new StringReader(text);
		try {
			parser.parse(new InputSource(reader));
			Transformer transformer = TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
			transformer.setOutputProperty(OutputKeys.METHOD, "xml");
			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
			transformer.transform(new DOMSource(parser.getDocument()), new StreamResult(writer));
		} catch (SAXException e) {
			throw new FilterException("SAXException", e);
		} catch (IOException e) {
			throw new FilterException("IOException", e);
		} catch (TransformerConfigurationException e) {
			throw new FilterException("TransformerConfigurationException", e);
		} catch (TransformerFactoryConfigurationError e) {
			throw new FilterException("TransformerFactoryConfigurationError", e);
		} catch (TransformerException e) {
			throw new FilterException("TransformerException", e);
		}
		return writer.toString();
	}

	/**
	 * Returns the state of the feature for the NekoHTML parser.
	 * @param feature The unique identifier (URI) of the feature.
	 * @throws FilterException If the feature was not a recognized feature or the feature is not supported.
	 */
	public boolean getFeature(final String feature) throws FilterException {
		try {
			return parser.getFeature(feature);
		} catch (SAXNotRecognizedException e) {
			throw new FilterException("SAXNotRecognizedException", e);
		} catch (SAXNotSupportedException e) {
			throw new FilterException("SAXNotSupportedException", e);
		}
	}

	/**
	 * Returns the value of a property for the NekoHTML parser.
	 * @param property The unique identifier (URI) of the property.
	 * @throws FilterException If the property was not a recognized property or the property is not supported.
	 */
	public Object getProperty(final String property) throws FilterException {
		try {
			return parser.getProperty(property);
		} catch (SAXNotRecognizedException e) {
			throw new FilterException("SAXNotRecognizedException", e);
		} catch (SAXNotSupportedException e) {
			throw new FilterException("SAXNotSupportedException", e);
		}
	}

	/**
	 * Initializes default settings.
	 */
	private void initDefaultSettings() {
		try {
			parser.setFeature("http://xml.org/sax/features/namespaces", false);
			//parser.setFeature("http://apache.org/xml/features/scanner/notify-char-refs", true);
			//parser.setFeature("http://apache.org/xml/features/scanner/notify-builtin-refs", true);
			//parser.setFeature("http://cyberneko.org/html/features/scanner/notify-builtin-refs", true);
			// parser.setFeature("http://cyberneko.org/html/features/scanner/ignore-specified-charset", true);
			parser.setFeature("http://cyberneko.org/html/features/augmentations", true);
			parser.setFeature("http://cyberneko.org/html/features/balance-tags/document-fragment", true);
			parser.setFeature("http://cyberneko.org/html/features/balance-tags", true);
			parser.setFeature("http://cyberneko.org/html/features/scanner/fix-mswindows-refs", true);
			parser.setProperty("http://cyberneko.org/html/properties/names/elems", "lower");
			parser.setProperty("http://cyberneko.org/html/properties/names/attrs", "lower");
			parser.setProperty("http://cyberneko.org/html/properties/default-encoding", "UTF-8");
		} catch (SAXNotRecognizedException e) {
			logger.error("SAXNotRecognizedException", e);
		} catch (SAXNotSupportedException e) {
			logger.error("SAXNotSupportedException", e);
		}
	}

	/**
	 * Sets a feature on the NekoHTML parser.
	 * @param feature The unique identifier (URI) of the feature.
	 * @param state The requested state of the feature (true or false).
	 * @throws FilterException If the feature was not a recognized feature or the feature is not supported.
	 */
	public void setFeature(final String feature, final boolean state) throws FilterException {
		try {
			parser.setFeature(feature, state);
		} catch (SAXNotRecognizedException e) {
			throw new FilterException("SAXNotRecognizedException", e);
		} catch (SAXNotSupportedException e) {
			throw new FilterException("SAXNotSupportedException", e);
		}
	}

	/**
	 * Sets a property on the NekoHTML parser.
	 * @param property The unique identifier (URI) of the property.
	 * @param value The value to set the property to.
	 * @throws FilterException If the property was not a recognized property or the property is not supported.
	 */
	public void setProperty(final String property, final Object value) throws FilterException {
		try {
			parser.setProperty(property, value);
		} catch (SAXNotRecognizedException e) {
			throw new FilterException("SAXNotRecognizedException", e);
		} catch (SAXNotSupportedException e) {
			throw new FilterException("SAXNotSupportedException", e);
		}
	}
}
